import type { PluginOption } from "vite";
import { normalizePath } from "vite";
import { init, parse } from "es-module-lexer";
import type { ImportSpecifier } from "es-module-lexer";
import { createFilter } from "@rollup/pluginutils";
import { camelCase, capitalCase, constantCase, dotCase, headerCase, noCase, paramCase, pascalCase, pathCase, sentenceCase, snakeCase } from "change-case";
import { createRequire } from "module";
import path from "pathe";
import fs from "fs-extra";

const ensureFileExts: string[] = [".css", ".js", ".scss", ".less", ".styl"];

function isRegExp(value: unknown) {
  return Object.prototype.toString.call(value) === "[object RegExp]";
}

function isFunction(value: any): value is (...args: any[]) => any {
  return value != null && Object.prototype.toString.call(value) === "[object Function]";
}

function getLib(libraryName: string, libs: Lib[], external?: any) {
  let libList = libs;
  if (external) {
    const isString = typeof external === "string";
    const isRE = isRegExp(external);
    if (isString) {
      libList = libList.filter((item) => item.libraryName !== external);
    } else if (isRE) {
      libList = libList.filter((item) => !(external as RegExp).test(item.libraryName));
    } else if (Array.isArray(external)) {
      libList = libList.filter((item) => {
        return !external.some((val) => {
          if (typeof val === "string") {
            return val === item.libraryName;
          }
          return (val as RegExp).test(item.libraryName);
        });
      });
    }
  }
  return libList.find((item) => item.libraryName === libraryName);
}

function transformImportVar(importStr: string) {
  if (!importStr) {
    return [];
  }
  const asRE = /\s+as\s+\w+,?/g;
  const exportStr = importStr.replace("import", "export").replace(asRE, ",");
  let importVariables: string[] = [];
  try {
    for (let item of parse(exportStr)[1]) {
      importVariables.push(item["n"]);
    }
  } catch (error) {
    console.log(error);
  }
  return importVariables;
}

function filterImportVariables(importVars: string[], reg?: RegExp) {
  if (!reg) {
    return importVars;
  }
  return importVars.filter((item) => reg.test(item));
}

function getChangeCaseFileName(importedName: string, libraryNameChangeCase: string) {
  let resImportName = "";
  try {
    switch (libraryNameChangeCase) {
      case "camelCase":
        {
          resImportName = camelCase(importedName);
        }
        break;
      case "capitalCase":
        {
          resImportName = capitalCase(importedName);
        }
        break;
      case "constantCase":
        {
          resImportName = constantCase(importedName);
        }
        break;
      case "dotCase":
        {
          resImportName = dotCase(importedName);
        }
        break;
      case "headerCase":
        {
          resImportName = headerCase(importedName);
        }
        break;
      case "noCase":
        {
          resImportName = noCase(importedName);
        }
        break;
      case "paramCase":
        {
          resImportName = paramCase(importedName);
        }
        break;
      case "pascalCase":
        {
          resImportName = pascalCase(importedName);
        }
        break;
      case "pathCase":
        {
          resImportName = pathCase(importedName);
        }
        break;
      case "sentenceCase":
        {
          resImportName = sentenceCase(importedName);
        }
        break;
      case "snakeCase":
        {
          resImportName = snakeCase(importedName);
        }
        break;
    }
    return resImportName;
  } catch (error) {
    return importedName;
  }
}

function resolveNodeModules(libName: string, ...dir: string[]) {
  const esRequire = createRequire(import.meta.url);
  let modulePath = "";
  let nodeModulesStr = "";
  try {
    modulePath = normalizePath(esRequire.resolve(libName));
  } catch (error) {
    nodeModulesStr = "node_modules";
    modulePath = normalizePath(path.resolve(libName));
  }
  const lastIndex = modulePath.lastIndexOf(libName);
  return normalizePath(path.resolve(modulePath.substring(0, lastIndex), nodeModulesStr, ...dir));
}

function fileExists(f: string) {
  try {
    fs.accessSync(f, fs.constants.W_OK);
    return true;
  } catch (error) {
    return false;
  }
}

function ensureFileExists(libraryName: string, importStr: string, esModule = false) {
  const extName = path.extname(importStr);
  if (!extName) {
    return tryEnsureFile(libraryName, importStr, esModule);
  }

  if (esModule) {
    return fileExists(importStr);
  }

  return true;
}

function tryEnsureFile(libraryName: string, filePath: string, esModule = false) {
  const filePathList = ensureFileExts.map((item) => {
    const p = `${filePath}${item}`;
    return esModule ? p : resolveNodeModules(libraryName, p);
  });
  return filePathList.some((item) => fileExists(item));
}

async function transformComponentCss(lib: Lib, importVariables: readonly string[]) {
  const { libraryName, resolveStyle, esModule, libraryNameChangeCase = "paramCase", ensureStyleFile = false } = lib;

  if (!isFunction(resolveStyle) || !libraryName) {
    return [];
  }
  const set = new Set<string>();
  for (let index = 0; index < importVariables.length; index++) {
    const name = getChangeCaseFileName(importVariables[index], libraryNameChangeCase);
    let importStr = resolveStyle(name);
    if (!importStr) {
      continue;
    }
    let isAdd = true;
    let tempImportStr = importStr;
    if (esModule) {
      tempImportStr = resolveNodeModules(libraryName, importStr);
    }
    if (ensureStyleFile) {
      isAdd = ensureFileExists(libraryName, tempImportStr, esModule);
    }
    isAdd && set.add(`import '${importStr}';\n`);
  }
  return Array.from(set);
}

export default function vitePluginTemplate(options: any = {}): PluginOption {
  const { include = ["**/*.vue", "**/*.ts", "**/*.js", "**/*.tsx", "**/*.jsx"], exclude = "node_modules/**", resolves = [] } = options;

  let { libs = [] } = options;
  libs = [...libs, ...resolves];
  const filter = createFilter(include, exclude);
  let external: any;
  return {
    name: "vite:vite-plugin-unplugin-css",
    enforce: "post",
    configResolved(resolvedConfig) {
      external = resolvedConfig?.build?.rollupOptions?.external ?? [];
    },
    async transform(code, id) {
      if (!code || !filter(id) || !needTransform(code, libs)) {
        return null;
      }
      await init;
      let imports: readonly ImportSpecifier[] = [];
      try {
        imports = parse(code)[0];
      } catch (e) {
        console.error(e);
      }
      if (!imports.length) {
        return null;
      }
      let str = code;
      for (let index = 0; index < imports.length; index++) {
        const { n, se, ss } = imports[index];
        if (!n) continue;
        const lib = getLib(n, libs, external);
        if (!lib) continue;
        const importStr = code.slice(ss, se);
        let importVariables = transformImportVar(importStr);
        importVariables = filterImportVariables(importVariables, lib.importTest);
        const importCssStrList = await transformComponentCss(lib, importVariables);
        const compStrList: string[] = [];
        const { base = "" } = lib;
        let baseImporter = base ? "\n" + `import '${base}'` : "";
        if (str.indexOf(base) > -1) {
          baseImporter = "";
        }
        const endIndex = se + 1;
        str = str.slice(0, endIndex) + `${baseImporter}\n${compStrList.join("")}${importCssStrList.join("")}` + str.slice(endIndex);
      }
      return {
        map: null,
        code: str,
      };
    },
  };
}

function needTransform(code: string, libs: any[]) {
  return !libs.every(({ libraryName }) => {
    return !new RegExp(`('${libraryName}')|("${libraryName}")`).test(code);
  });
}
